/*
 * Copyright 2008 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.googlecode.gwt.charts.client.ajaxloader;

import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArrayString;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.ScriptElement;
import com.google.gwt.user.client.Window;

import java.util.Vector;

/**
 * A wrapper for the google Ajax API loader.
 * 
 * http://code.google.com/apis/ajax/documentation/
 */
public class AjaxLoader {
	/**
	 * An object to pass as the third parameter to load() call.
	 */
	public static class AjaxLoaderOptions extends JavaScriptObject {
		public static AjaxLoaderOptions newInstance() {
			return JavaScriptObject.createObject().cast();
		}

		protected AjaxLoaderOptions() {
			// A protected constructor is required for JavaScriptObject
			// overlays.
		}

		public final native void setBaseDomain(String baseDomain) /*-{
			this.base_domain = baseDomain;
		}-*/;

		public final native void setLanguage(String language) /*-{
			this.language = language;
		}-*/;

		public final native void setNoCss(boolean value) /*-{
			this.nocss = value;
		}-*/;

		public final native void setOtherParms(String otherParams) /*-{
			this.other_params = otherParams;
		}-*/;

		public final native void setPackages(JsArrayString packages) /*-{
			this.packages = packages;
		}-*/;

		public final void setPackages(String... packages) {
			setPackages(ArrayHelper.toJsArrayString(packages));
		}

		private native void setCallback(Runnable onLoad) /*-{
			this.callback = function() {
				@com.googlecode.gwt.charts.client.ajaxloader.ExceptionHelper::runProtected(Ljava/lang/Runnable;)(onLoad);
			}
		}-*/;
	}

	// NativeCreateCallback already ran, or someone injected the API outside of
	// this program.
	static boolean alreadyInjected = false;

	// Set to true if the init(key) method has been called.
	static boolean initialized = false;

	// True if the JavaScript __gwt_AjaxLoader_onLoad callback has already run.
	// This function is registered on the window in nativeCreateCallback()
	static boolean loaded = false;

	static Vector<Runnable> queuedApiLoads = new Vector<Runnable>();

	public static ClientLocation getClientLocation() {
		if (!injectJsapi(null, null)) {
			return null;
		}
		return nativeGetClientLocation();
	}

	/**
	 * Initialize the API without specifying a key.
	 */
	public static void init() {
		init(null);
	}

	/**
	 * Initialize the API with a supplied key value. See
	 * http://code.google.com/apis/ajaxsearch/signup.html
	 * 
	 * @param apiKey
	 *            API key value.
	 */
	public static void init(String apiKey) {
		init(apiKey, null);
	}

	/**
	 * Initialize the API with a supplied key value and custom hostname. Most
	 * programmers should use {@link AjaxLoader#init(String)} instead, as it
	 * covers almost all cases of using AjaxLoader API.
	 * 
	 * See http://code.google.com/apis/ajaxsearch/signup.html
	 * 
	 * @param apiKey
	 *            API key value.
	 * @param hostname
	 *            Custom hostname (e.g. "www.google.com").
	 */
	public static void init(String apiKey, String hostname) {
		if (initialized == true) {
			return;
		}

		if (apiKey == null) {
			// No key was specified. Use the location in the page to lookup a
			// key from
			// the Key repository
			apiKey = AjaxKeyRepository.getKey();
		}

		boolean alreadyLoaded = injectJsapi(apiKey, hostname);

		// In IE, the above script can execute immediately if its already in the
		// cache, so don't touch the loaded variable unless we bypassed loading
		// the
		// script completely
		if (alreadyLoaded) {
			loaded = true;
		}
		initialized = true;
	}

	/**
	 * Launches an API load request.
	 * 
	 * @param api
	 *            The name of the API to load
	 * @param version
	 *            The API version to load
	 * @param onLoad
	 *            A callback that will be invoked when the API is finished
	 *            loaded. Do not make any calls into the API being loaded until
	 *            this call returns.
	 * @param settings
	 *            An object containing additional settings.
	 */
	public static void loadApi(final String api, final String version,
			Runnable onLoad, AjaxLoaderOptions settings) {

		// Initialize the API if it hasn't already been initialized.
		init();

		// Set the onLoad callback into the
		assert (onLoad != null);
		if (settings == null) {
			settings = AjaxLoaderOptions.newInstance();
		}
		settings.setCallback(onLoad);
		final AjaxLoaderOptions copyOfSettings = settings;

		// Define a Runnable that will run the actual load.
		Runnable apiLoad = new Runnable() {

			@Override
			public void run() {
				nativeLoadApi(api, version, copyOfSettings);
			}
		};

		if (loaded) {
			// jsapi is finished loading, start the individual API load now.
			apiLoad.run();
		} else {
			// Defer the load until jsapi is finished.
			queuedApiLoads.add(apiLoad);
		}
	}

	private static String getProtocol() {
		if (Window.Location.getProtocol().equals("https:")) {
			return "https:";
		}
		return "http:";
	}

	/**
	 * Adds a script element to the DOM that loads the Ajax API Loader main
	 * script "jsapi".
	 * 
	 * @param apiKey
	 *            Optional API key value (pass null to omit the key). See
	 *            http://code.google.com/apis/ajaxsearch/signup.html
	 * @returns <code>true</code> if the API has already been loaded. Otherwise,
	 *          returns <code>false</code>, meaning that the application should
	 *          wait for a callback.
	 */
	private static boolean injectJsapi(String apiKey, String hostname) {
		if (alreadyInjected) {
			return true;
		}
		boolean alreadyLoaded = nativeCreateCallback();
		alreadyInjected = true;
		if (alreadyLoaded) {
			return true;
		}
		Document doc = Document.get();
		String key = (apiKey == null) ? "" : ("key=" + apiKey + "&");
		hostname = (hostname == null) ? "www.google.com" : hostname;
		String src = getProtocol() + "//" + hostname + "/jsapi?" + key
				+ "callback=__gwt_AjaxLoader_onLoad";
		ScriptElement script = doc.createScriptElement();
		script.setSrc(src);
		script.setType("text/javascript");
		doc.getBody().appendChild(script);
		return false;
	}

	/**
	 * Creates a function to be registered for a callback after jsapi loads.
	 */
	private static native boolean nativeCreateCallback() /*-{
		if ($wnd['google'] && $wnd.google['load']) {
			// The API has already been loaded.
			return true;
		}
		$wnd.__gwt_AjaxLoader_onLoad = function() {
			@com.googlecode.gwt.charts.client.ajaxloader.AjaxLoader::onLoadCallback()();
		}
		// The application must wait for a callback.
		return false;
	}-*/;

	private static native ClientLocation nativeGetClientLocation() /*-{
		return $wnd.google.loader.ClientLocation;
	}-*/;

	/**
	 * Wrapper for AjaxLoader google.load() native method.
	 */
	private static native void nativeLoadApi(String api, String version,
			JavaScriptObject settings) /*-{
		$wnd.google.load(api, version, settings);
	}-*/;

	/**
	 * Called back when the jsapi is finished loaded. It must kick of any API
	 * loads that have been queued while waiting on jsapi to finish loading.
	 */
	private static void onLoadCallback() {
		loaded = true;

		for (Runnable r : queuedApiLoads) {
			r.run();
		}
		queuedApiLoads.clear();
	}

	private AjaxLoader() {
		// This class only contains static methods and should not be
		// instantiated.
	}
}
